package com.sunday.xxl.job.trace;

import brave.Span;
import brave.Tracer;
import brave.Tracing;
import brave.propagation.CurrentTraceContext;
import brave.propagation.TraceContext;
import com.xxl.job.core.context.XxlJobContext;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

import static brave.Span.Kind.CLIENT;

/**
 * XxlJob 添加 brave 链路追踪
 * <p>
 * Tracing、Tracer和Span都是与分布式系统中跟踪请求的相关概念。
 * <p>
 * Tracing（分布式追踪）是一种技术，用于收集并展示分布式系统中跨越多个服务的请求的数据，以便诊断性能问题和故障排除。
 * Tracer（追踪器）是一种库或工具，用于在代码中创建和记录跟踪信息。它通常与分布式追踪系统配合使用，例如Jaeger或Zipkin等。
 * Span（跨度）表示一个操作的时间跨度，例如从发起请求到接收响应的整个时间。Span包含有关操作的信息，例如开始和结束时间戳，持续时间，标签和日志等。Span是组成分布式追踪数据的基本单元。
 *
 * @author zsy
 * @since 2022/10/23
 */
@Slf4j
@Aspect
public class TraceXxlJobAspect {

    final Tracer tracer;

    final CurrentTraceContext currentTraceContext;

    public TraceXxlJobAspect(Tracing tracing) {
        this.tracer = tracing.tracer();
        this.currentTraceContext = tracing.currentTraceContext();
    }

    /**
     * xxl job tracer
     * <p>
     * Tracer.currentSpan()，而且还允许自定义设置（例如SLF4J MDC）查看当前的跟踪ID。
     * 在Sleuth中，可以Tracer通过tracer.currentSpan()方法自动装配Bean以检索当前范围 。要检索当前上下文，只需调用 tracer.currentSpan().context()。要以String的形式获取当前跟踪ID，可以使用以下traceIdString()方法：tracer.currentSpan().context().traceIdString()。
     * <p>
     * Tracer.withSpanInScope(Span)通过使用try-with-resources惯用语可以方便地进行此操作，并且可以最方便地使用它。每当可能调用外部代码（例如进行拦截器或其他操作）时，请将范围放在范围内，如以下示例所示：
     * <p>
     * 跨度生命周期
     * 您可以通过以下方式在Span上执行以下操作brave.Tracer：
     * start：开始跨度时，将分配其名称并记录开始时间戳。
     * close：跨度完成（记录了跨度的结束时间），并且，如果对跨度进行了采样，则可以进行收集（例如，收集到Zipkin）。
     * continue：将创建一个新的span实例。它是继续的副本。
     * detach：跨度不会停止或关闭。它只会从当前线程中删除。
     * create with explicit parent(使用显式父项创建)：您可以创建一个新的跨度并为其设置一个显式父项。
     * <p>
     * xxl job
     * <p>
     * 1、任务开发：在Spring Bean实例中，开发Job方法；
     * 2、注解配置：为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")"，注解value值对应的是调度中心新建任务的JobHandler属性的值。
     * 3、执行日志：需要通过 "XxlJobHelper.log" 打印执行日志；
     * 4、任务结果：默认任务结果为 "成功" 状态，不需要主动设置；如有诉求，比如设置任务结果为失败，可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果；
     *
     * @param pjp
     * @return {@link Object}
     */
    @Around("execution (@com.xxl.job.core.handler.annotation.XxlJob  * *.*(..))")
    public Object aroundMethod(ProceedingJoinPoint pjp) {

        Object response = null;

        TraceContext maybeParent = currentTraceContext.get();
        Span span;
        if (maybeParent == null) {
            span = tracer.nextSpan();
        } else {
            span = tracer.newChild(maybeParent);
        }

        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        String service = pjp.getTarget().getClass().getName();
        if (!span.isNoop()) {
            XxlJob xxlJob = methodSignature.getMethod().getAnnotation(XxlJob.class);
            span.kind(CLIENT).name(TraceConstants.FROM_PREFIX + TraceConstants.XXL_JOB_SERVICE);
            maybeTag(span, TraceConstants.JOB_ID, String.valueOf(XxlJobContext.getXxlJobContext().getJobId()));
            maybeTag(span, TraceConstants.JOB_HANDLER, xxlJob == null ? null : xxlJob.value());
            maybeTag(span, TraceConstants.CLASS_NAME, service);
            span.start();
        }

        Tracer.SpanInScope ws = tracer.withSpanInScope(span);
        try {
            XxlJobHelper.log(TraceConstants.request_xxl_job_format, span.context(), XxlJobHelper.getJobParam(), service, methodSignature.getName());
            log.info(TraceConstants.request_format, XxlJobHelper.getJobParam(), service, methodSignature.getName());

            response = pjp.proceed();

            XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext();
            if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobContext.getHandleCode()) {
                XxlJobHelper.log(TraceConstants.response_xxl_job_format_ack, span.context(), xxlJobContext.getHandleCode(), xxlJobContext.getHandleMsg());
                log.info(TraceConstants.response_format_ack, xxlJobContext.getHandleCode(), xxlJobContext.getHandleMsg());
            } else {
                XxlJobHelper.log(TraceConstants.response_xxl_job_format_nack, span.context(), xxlJobContext.getHandleCode(), xxlJobContext.getHandleMsg());
                log.error(TraceConstants.response_format_nack, xxlJobContext.getHandleCode(), xxlJobContext.getHandleMsg());
            }
        } catch (Throwable ex) {
            XxlJobHelper.handleFail(ex.toString());
            XxlJobHelper.log(TraceConstants.response_xxl_job_format_throwable, span.context(), ex.toString(), ex);
            log.error(TraceConstants.response_format_throwable, ex.toString(), ex);
            //除非您处理异常，否则您可能不知道操作失败!
            span.error(ex); //报错处理
        } finally {
            //始终完成跨度
            //注意，范围独立于跨度。一定要完成一段时间。
            span.finish(); //结束跨度 span
            ws.close();
        }
        return response;
    }

    void maybeTag(Span span, String tag, String value) {
        if (value != null) span.tag(tag, value);
    }

}
